/**
 * Anderground project - Subway maps processor application.
 * Copyright (C) 2008-2009 Eldar Abusalimov, Ilya Shurigyn
 * 
 * This file is part of Anderground.
 * 
 * Anderground is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * Anderground is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Anderground.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.anderground.core.model;

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.anderground.core.util.Bindable;
import org.anderground.core.util.Binder;
import org.anderground.core.util.BindingException;
import org.anderground.core.util.WeakObservable;

/**
 * Contains a subway data representation and methods to process it
 * 
 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
 */
public final class ModelData extends WeakObservable implements Serializable {

	/**
	 * Provides an access for editing the {@link ModelData} instance.
	 * 
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	public class Editor {

		/** the callback for event fired on adding a {@link Bindable} */
		private OnAddCallback onAddCallback;

		/** the callback for event fired on removing a {@link Bindable} */
		private OnRemoveCallback onRemoveCallback;

		/**
		 * Private constructor
		 */
		private Editor() {
		}

		/**
		 * By default, the model data is builded when you invoke methods that
		 * depend on this {@link ModelData} consistency such as
		 * {@link #getHubs()} or {@link Station#getHub()}.
		 * <p>
		 * This method allows you to override the default behavior and force
		 * instant building of the data.
		 */
		public void forceRebuild() {
			rebuild();
		}

		/**
		 * Fires {@link OnAddCallback#onAdd(Station)} method.
		 * 
		 * @param station
		 */
		private void fireOnAddCallback(Station station) {
			if (onAddCallback != null) {
				onAddCallback.onAdd(station);
			}
		}

		/**
		 * Fires {@link OnAddCallback#onAdd(Transition)} method.
		 * 
		 * @param transition
		 */
		private void fireOnAddCallback(Transition transition) {
			if (onAddCallback != null) {
				onAddCallback.onAdd(transition);
			}
		}

		/**
		 * Fires {@link OnRemoveCallback#onRemove(Station)} method.
		 * 
		 * @param station
		 */
		private void fireOnRemoveCallback(Station station) {
			if (onRemoveCallback != null) {
				onRemoveCallback.onRemove(station);
			}
		}

		/**
		 * Fires {@link OnRemoveCallback#onRemove(Transition)} method.
		 * 
		 * @param transition
		 */
		private void fireOnRemoveCallback(Transition transition) {
			if (onRemoveCallback != null) {
				onRemoveCallback.onRemove(transition);
			}
		}

		/**
		 * @param bindable
		 * @param callback
		 * @throws ModelException
		 */
		private void internalAdd(Bindable bindable, OnAddCallback callback)
				throws ModelException {
			if (bindable == null) {
				throw new IllegalArgumentException(
						"bindable being added cannot be null");
			}
			ModelData.this.editor = this;
			try {
				this.onAddCallback = callback;
				bindable.bindTo(binder);
			} catch (BindingException e) {
				throw new ModelException("error adding bindable " + bindable, e);
			} finally {
				this.onAddCallback = null;
				notifyObservers();
			}
		}

		/**
		 * @param bindable
		 * @param callback
		 * @throws ModelException
		 */
		private void internalRemove(Bindable bindable, OnRemoveCallback callback)
				throws ModelException {
			if (bindable == null) {
				throw new IllegalArgumentException(
						"bindable being removed cannot be null");
			}
			ModelData.this.editor = this;
			try {
				this.onRemoveCallback = callback;
				bindable.unbindFrom(binder);
			} catch (BindingException e) {
				throw new ModelException("error removing bindable " + bindable,
						e);
			} finally {
				this.onRemoveCallback = null;
				notifyObservers();
			}
		}

		/**
		 * 
		 * @param city
		 */
		public void setCity(City city) {
			if (city == null) {
				throw new IllegalArgumentException(
						"model data city cannot be null");
			}
			ModelData.this.city = city;
		}

		/**
		 * 
		 * @param description
		 */
		public void setDescription(Description description) {
			if (description == null) {
				throw new IllegalArgumentException(
						"model data description cannot be null");
			}
			ModelData.this.description = description;
		}

		/**
		 * @param station
		 * @param callback
		 * @throws ModelException
		 */
		public void add(Station station, OnAddCallback callback)
				throws ModelException {
			internalAdd(station, callback);
		}

		/**
		 * @param transition
		 * @param callback
		 * @throws ModelException
		 */
		public void add(Transition transition, OnAddCallback callback)
				throws ModelException {
			internalAdd(transition, callback);
		}

		/**
		 * @param station
		 * @param callback
		 * @throws ModelException
		 */
		public void remove(Station station, OnRemoveCallback callback)
				throws ModelException {
			internalRemove(station, callback);
		}

		/**
		 * @param transition
		 * @param callback
		 * @throws ModelException
		 */
		public void remove(Transition transition, OnRemoveCallback callback)
				throws ModelException {
			internalRemove(transition, callback);
		}

		/**
		 * @param station
		 * @throws ModelException
		 */
		public void add(Station station) throws ModelException {
			internalAdd(station, null);
		}

		/**
		 * @param transition
		 * @throws ModelException
		 */
		public void add(Transition transition) throws ModelException {
			internalAdd(transition, null);
		}

		/**
		 * @param station
		 * @throws ModelException
		 */
		public void remove(Station station) throws ModelException {
			internalRemove(station, null);
		}

		/**
		 * @param transition
		 * @throws ModelException
		 */
		public void remove(Transition transition) throws ModelException {
			internalRemove(transition, null);
		}

		/**
		 * @return the {@link ModelData} instance being edited
		 */
		public final ModelData getModelData() {
			return ModelData.this;
		}

	}

	/**
	 * Used for constructing and initial modifying such complex object as
	 * {@link ModelData}.
	 * <p>
	 * To add objects to the model being builded use corresponding methods, e.g.
	 * {@link Builder#add(Station)}. To get the building target, call
	 * {@link Builder#getModelData()}.
	 * 
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	public static final class Builder extends Editor {

		/**
		 * The default constructor.
		 */
		public Builder() {
			new ModelData().super();
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	public static interface OnAddCallback {

		/**
		 * Called when a station is being added to the model.
		 * 
		 * @param station
		 *            the station being added
		 */
		public void onAdd(Station station);

		/**
		 * Called when a transition is being added to the model.
		 * 
		 * @param transition
		 */
		public void onAdd(Transition transition);

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	public static interface OnRemoveCallback {

		/**
		 * Called when a station is being removed from the model
		 * 
		 * @param station
		 *            the station being added
		 */
		public void onRemove(Station station);

		/**
		 * Called when a transition is being removed from the model, by either
		 * explicit or implicit request
		 * 
		 * @param transition
		 */
		public void onRemove(Transition transition);

	}

	private static final long serialVersionUID = -7651840119072826001L;

	/** the underground stations */
	/* transient */private Set<Station> stations = new HashSet<Station>();

	/** the underground transfers */
	/* transient */private Set<Transfer> transfers = new HashSet<Transfer>();

	/** the underground tunnels */
	/* transient */private Set<Tunnel> tunnels = new HashSet<Tunnel>();

	/** the underground hubs */
	/* transient */private Set<Hub> hubs = new HashSet<Hub>();

	/** the underground lines */
	/* transient */private Set<Line> lines = new HashSet<Line>();

	/** unmodifiable set backed by the {@link #stations} set */
	/* transient */private Set<Station> unmodifiableStationsSet = Collections
			.unmodifiableSet(stations);

	/** unmodifiable set backed by the {@link #transfers} set */
	/* transient */private Set<Transfer> unmodifiableTransfersSet = Collections
			.unmodifiableSet(transfers);

	/** unmodifiable set backed by the {@link #tunnels} set */
	/* transient */private Set<Tunnel> unmodifiableTunnelsSet = Collections
			.unmodifiableSet(tunnels);

	/** unmodifiable set backed by the {@link #hubs} set */
	/* transient */private Set<Hub> unmodifiableHubsSet = Collections
			.unmodifiableSet(hubs);

	/** unmodifiable set backed by the {@link #lines} set */
	/* transient */private Set<Line> unmodifiableLinesSet = Collections
			.unmodifiableSet(lines);

	/** the model city */
	/* transient */private City city = new City();

	/** the model description */
	/* transient */private Description description = new Description();

	/* transient */private List<Station> hublessStations = new LinkedList<Station>();

	/* transient */private Editor editor = new Editor();

	/** the binder to use when operating with stations and transitions */
	/* transient */private Binder binder = new Binder() {

		/** {@inheritDoc} */
		@Override
		public void bind(Bindable bindable) {
			if (bindable instanceof Station) {
				Station station = (Station) bindable;
				if (!stations.add(station)) {
					assert false;
				}
				editor.fireOnAddCallback(station);

			} else if (bindable instanceof Transition) {
				Transition transition = (Transition) bindable;
				if (transition instanceof Tunnel) {
					lines.add(((Tunnel) transition).getLine());
					if (!tunnels.add((Tunnel) transition)) {
						assert false;
					}
				}
				if (transition instanceof Transfer) {
					// TODO this may be unsafe
					hublessStations.add(transition.getFromStation());
					hublessStations.add(transition.getToStation());
					if (!transfers.add((Transfer) transition)) {
						assert false;
					}
				}
				editor.fireOnAddCallback(transition);

			} else if (bindable instanceof Hub) {
				Hub hub = (Hub) bindable;
				if (!hubs.add(hub)) {
					assert false;
				}

			}
			setChanged();
		}

		/** {@inheritDoc} */
		@Override
		public void unbind(Bindable bindable) {
			if (bindable instanceof Station) {
				Station station = (Station) bindable;
				if (!stations.remove(station)) {
					assert false;
				}
				hublessStations.remove(station);
				editor.fireOnRemoveCallback(station);

			} else if (bindable instanceof Transition) {
				Transition transition = (Transition) bindable;
				if (transition instanceof Tunnel) {
					boolean leaveLine = false;
					Line line = ((Tunnel) transition).getLine();
					for (Tunnel eachTunnel : tunnels) {
						if (eachTunnel != bindable
								&& eachTunnel.getLine() == line) {
							leaveLine = true;
							break;
						}
					}
					if (!leaveLine) {
						if (!lines.remove(line)) {
							assert false;
						}
					}
				}
				if (!tunnels.remove(transition)
						&& !transfers.remove(transition)) {
					assert false;
				}
				editor.fireOnRemoveCallback(transition);

			} else if (bindable instanceof Hub) {
				Hub hub = (Hub) bindable;
				if (!hubs.remove(hub)) {
					assert false;
				}
				hublessStations.addAll(hub.getStations());

			}
			setChanged();
		}

		/** {@inheritDoc} */
		@Override
		public void ensureConsistency() {
			rebuild();
		}

	};

	/**
	 * default constructor
	 */
	public ModelData() {
	}

	/**
	 * rebuilds lines and hubs
	 */
	private void rebuild() {

		for (Station station : hublessStations) {
			Hub hub = Hub.createHubIfNeeded(station);
			if (hub != null) {
				try {
					hub.bindTo(binder);
				} catch (BindingException e) {
					assert false;
				}
			}
		}
		hublessStations.clear();

	}

	/**
	 * @return the unmodifiable set of the underground {@link Station}s
	 */
	public Set<Station> getStations() {
		return unmodifiableStationsSet;
	}

	/**
	 * @return the unmodifiable set of the underground {@link Transfer}s
	 */
	public Set<Transfer> getTransfers() {
		return unmodifiableTransfersSet;
	}

	/**
	 * @return the unmodifiable set of the underground {@link Tunnel}s
	 */
	public Set<Tunnel> getTunnels() {
		return unmodifiableTunnelsSet;
	}

	/**
	 * @return the unmodifiable set of the underground {@link Hub}s
	 */
	public Set<Hub> getHubs() {
		editor.forceRebuild();
		return unmodifiableHubsSet;
	}

	/**
	 * @return the unmodifiable set of the underground {@link Line}s
	 */
	public Set<Line> getLines() {
		return unmodifiableLinesSet;
	}

	/**
	 * @return the description
	 */
	public Description getDescription() {
		return description;
	}

	/**
	 * @return the city
	 */
	public City getCity() {
		return city;
	}

	/**
	 * @return
	 */
	Editor getEditor() {
		return editor;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}

		ModelData another;
		if (obj instanceof ModelData) {
			another = (ModelData) obj;
		} else {
			return false;
		}

		if (!stations.equals(another.stations)) {
			return false;
		}
		if (!tunnels.equals(another.tunnels)) {
			return false;
		}
		if (!transfers.equals(another.transfers)) {
			return false;
		}
		if (!description.equals(another.description)) {
			return false;
		}
		if (!city.equals(another.city)) {
			return false;
		}

		return true;
	}

	/**
	 * Invoked when serializing this object.
	 * 
	 * @return {@link DataRep} instance needed to restore the data
	 * @throws ObjectStreamException
	 */
	private Object writeReplace() throws ObjectStreamException {
		return new DataRep(this);
	}

	/**
	 * Serial replacement of the {@link ModelData}.
	 * 
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static class DataRep implements Serializable {

		private static final long serialVersionUID = 315433201677562472L;

		/** the underground stations */
		private List<Station> stations;

		/** the underground transfers */
		private List<Transfer> transfers;

		/** the underground tunnels */
		private List<Tunnel> tunnels;

		/** the model description */
		private Description description;

		/** the model city */
		private City city;

		/**
		 * Constructs a new object that holds references to the data needed to
		 * be serialized.
		 * 
		 * @param modelData
		 */
		private DataRep(ModelData modelData) {
			stations = new ArrayList<Station>(modelData.getStations());
			transfers = new ArrayList<Transfer>(modelData.getTransfers());
			tunnels = new ArrayList<Tunnel>(modelData.getTunnels());
			description = modelData.getDescription();
			city = modelData.getCity();
		}

		/**
		 * Invoked when deserializing the model data.
		 * 
		 * @return the new {@link ModelData} instance
		 * @throws ObjectStreamException
		 */
		private Object readResolve() throws ObjectStreamException {
			Builder builder = new Builder();

			try {
				builder.setDescription(description);
				builder.setCity(city);
				for (Station station : stations) {
					builder.add(station);
				}
				for (Transition transition : transfers) {
					builder.add(transition);
				}
				for (Transition transition : tunnels) {
					builder.add(transition);
				}
			} catch (ModelException e) {
				throw new RuntimeException(
						"unexpected error during restoring the Model", e);
			}
			return builder.getModelData();
		}

	}

}
